package rpc.core.plugin.impl.proxy;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.util.CollectionUtils;
import rpc.core.FlowMap;
import rpc.core.FlowReq;
import rpc.core.FlowRes;
import rpc.core.annotations.db.BatchDelete;
import rpc.core.annotations.db.BatchInsert;
import rpc.core.annotations.db.BatchUpdate;
import rpc.core.annotations.db.DDL;
import rpc.core.annotations.db.Delete;
import rpc.core.annotations.db.Insert;
import rpc.core.annotations.db.InsertWithNewId;
import rpc.core.annotations.db.Select;
import rpc.core.annotations.db.SelectOne;
import rpc.core.annotations.db.Update;
import rpc.core.plugin.impl.proxy.SqlFormatUtils.SqlFormat;


public class DaoServiceProxy implements ServiceProxy {

  private Method method;
  private String annotationName;
  private Object annotation;
  private String flowName;
  private JdbcTemplate jdbcTemplate;

  private Map<String, Object> valueMap;

  private ExecutorService executorService;

  public JdbcTemplate getJdbcTemplate() {
    return jdbcTemplate;
  }

  public DaoServiceProxy setJdbcTemplate(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
    return this;
  }

  public DaoServiceProxy setExecutorService(ExecutorService executorService) {
    this.executorService = executorService;
    return this;
  }

  public Map<String, Object> getValueMap() {
    return valueMap;
  }

  public DaoServiceProxy setValueMap(Map<String, Object> valueMap) {
    this.valueMap = valueMap;
    return this;
  }

  public String getFlowName() {
    return flowName;
  }

  public DaoServiceProxy setFlowName(String flowName) {
    this.flowName = flowName;
    return this;
  }

  public Method getMethod() {
    return method;
  }

  public DaoServiceProxy setMethod(Method method) {
    this.method = method;
    return this;
  }

  public String getAnnotationName() {
    return annotationName;
  }

  public DaoServiceProxy setAnnotationName(String annotationName) {
    this.annotationName = annotationName;
    return this;
  }


  @Override
  public CompletableFuture<FlowRes> invoke(FlowReq req) {

    switch (annotationName) {
      case "SelectOne":
        return selectOne(req);
      case "BatchDelete":
      case "BatchInsert":
      case "BatchUpdate":
        return batchExec(req);
      case "Select":
        return select(req);
      case "DDL":
        return ddl(req);
      case "InsertWithNewId":
        return insertWithNewId(req);
      case "Insert":
      case "Update":
      case "Delete":
        return update(req);
      default:
        throw new RuntimeException("DB no Impl:" + annotation);
    }

  }

  private CompletableFuture<FlowRes> selectOne(FlowReq req) {

    CompletableFuture<FlowRes> future = new CompletableFuture<>();

    executorService.execute(() -> {
      SelectOne selectOne = (SelectOne) this.annotation;
      String sql = selectOne.value();
      SqlFormat sqlFormat = SqlFormatUtils.sqlFormat(sql);
      if (sqlFormat.hasParam()) {
        future.complete(new FlowRes(this.jdbcTemplate
            .query(sqlFormat.getSql(), sqlFormat.getFields().stream().map(req::o).toArray(),
                new MapMapper(selectOne.rowNumName()))));
      } else {
        future.complete(new FlowRes(this.jdbcTemplate.queryForMap(sql)));
      }
    });
    return future;
  }

  private CompletableFuture<FlowRes> batchExec(FlowReq req) {
    CompletableFuture<FlowRes> future = new CompletableFuture<>();
    executorService.execute(() -> {
      List<FlowMap> list = req.nlm("list");
      String sql = null;
      if (annotation instanceof BatchDelete) {
        sql = ((BatchDelete) annotation).value();
      } else if (annotation instanceof BatchInsert) {
        sql = ((BatchInsert) annotation).value();
      } else if (annotation instanceof BatchUpdate) {
        sql = ((BatchUpdate) annotation).value();
      }
      Objects.requireNonNull(sql, "batchExec sql must not null");
      SqlFormat sqlFormat = SqlFormatUtils.sqlFormat(sql);
      boolean hasParam = sqlFormat.hasParam();
      int[] intArray;
      if (!hasParam) {
        intArray = this.jdbcTemplate.batchUpdate(sql);
      } else {
        List<Object[]> paramsList = new ArrayList<>(list.size());
        List<String> fields = sqlFormat.getFields();
        list.forEach(item -> {
          List<Object> itemValue = new ArrayList<>(fields.size());
          fields.forEach(f -> itemValue.add(item.o(f)));
          paramsList.add(itemValue.toArray());
        });
        intArray = this.jdbcTemplate.batchUpdate(sqlFormat.getSql(), paramsList);
      }

      String details = StringUtils.join(intArray, ',');
      future.complete(new FlowRes().append("details", details));
    });
    return future;
  }

  private CompletableFuture<FlowRes> select(FlowReq req) {
    CompletableFuture<FlowRes> future = new CompletableFuture<>();
    executorService.execute(() -> {
      Select select = (Select) this.annotation;
      String sql = select.value();
      SqlFormat sqlFormat = SqlFormatUtils.sqlFormat(sql);
      final List<Map<String, Object>> list = new ArrayList<>();
      if (sqlFormat.hasParam()) {
        Object[] valueArray = sqlFormat.getFields().stream().map(req::o).toArray();
        this.jdbcTemplate.query(sqlFormat.getSql(), valueArray, getRowCallbackHandler(list));
      } else {
        this.jdbcTemplate.queryForList(sqlFormat.getSql(), getRowCallbackHandler(list));
      }
      future.complete(new FlowRes().append("list", list));
    });
    return future;
  }

  private CompletableFuture<FlowRes> ddl(FlowReq req) {
    CompletableFuture<FlowRes> future = new CompletableFuture<>();
    executorService.execute(() -> {
      DDL ddl = (DDL) annotation;
      String sql = ddl.value();
      SqlFormat sqlFormat = SqlFormatUtils.sqlFormat(sql);
      sql = sqlFormat.getSql();
      if (sqlFormat.hasParam()) {
        List<String> fields = sqlFormat.getFields();
        for (String f : fields) {
          sql = sql.replaceFirst("\\?", req.ns(f));
        }
      }
      jdbcTemplate.execute(sql);
      future.complete(new FlowRes().ok());
    });
    return future;
  }

  private CompletableFuture<FlowRes> insertWithNewId(FlowReq req) {
    CompletableFuture<FlowRes> future = new CompletableFuture<>();
    executorService.execute(() -> {
      InsertWithNewId insertWithNewId = (InsertWithNewId) annotation;
      String sql = insertWithNewId.value();
      SqlFormat sqlFormat = SqlFormatUtils.sqlFormat(sql);
      List<Object> valueList = sqlFormat.getFields().stream().map(req::o)
          .collect(Collectors.toList());
      KeyHolder keyHolder = new GeneratedKeyHolder();
      if (sqlFormat.hasParam()) {
        this.jdbcTemplate.update(new DbPreparedStatementCreator(sqlFormat.getSql(), valueList), keyHolder);
      } else {
        this.jdbcTemplate.update(sql, keyHolder);
      }
      Number key = keyHolder.getKey();
      if (Objects.nonNull(key)) {
        future.complete(new FlowRes().append("id", key.longValue()));
      } else {
        future.complete(new FlowRes().fail(-6230).append("updateCount=0"));
      }
    });
    return future;
  }

  private CompletableFuture<FlowRes> update(FlowReq req) {
    CompletableFuture<FlowRes> future = new CompletableFuture<>();
    executorService.execute(() -> {
      String sql = null;
      if (annotation instanceof Insert) {
        Insert insert = (Insert) annotation;
        sql = insert.value();
      } else if (annotation instanceof Update) {
        Update update = (Update) annotation;
        sql = update.value();
      } else if (annotation instanceof Delete) {
        Delete delete = (Delete) annotation;
        sql = delete.value();
      }

      Objects.requireNonNull(sql, "update sql must not null");

      SqlFormat sqlFormat = SqlFormatUtils.sqlFormat(sql);
      if (sqlFormat.hasParam()) {
        Object[] valueArray = sqlFormat.getFields().stream().map(req::o).toArray();
        int update = this.jdbcTemplate.update(sqlFormat.getSql(), valueArray);
        future.complete(new FlowRes().append("updateCount", update));
      } else {
        int updateCount = this.jdbcTemplate.update(sql);
        future.complete(new FlowRes().append("updateCount", updateCount));
      }
    });

    return future;
  }

  private RowCallbackHandler getRowCallbackHandler(List<Map<String, Object>> list) {
    return rs -> {
      List<String> columnNames = new ArrayList<>();
      ResultSetMetaData meta = rs.getMetaData();
      int num = meta.getColumnCount();
      for (int i = 0; i < num; i++) {
        columnNames.add(meta.getColumnLabel(i + 1));
      }
      do {
        Map<String, Object> map = new HashMap<>();
        for (int i = 1; i <= num; i++) {
          Object object = rs.getString(i);
          map.put(columnNames.get(i - 1), object);
        }
        list.add(map);
      } while (rs.next());
    };
  }

  public DaoServiceProxy setAnnotation(Annotation annotation) {
    this.annotation = annotation;
    this.annotationName = annotation.annotationType().getSimpleName();
    return this;
  }

  private Map<String, Object> getStringObjectMap(ResultSet rs, String rowNumName, int rowNum)
      throws SQLException {
    ResultSetMetaData metaData = rs.getMetaData();
    int columnCount = metaData.getColumnCount();
    Map<String, Object> map = new HashMap<>(columnCount);
    List<String> columnNameList = new ArrayList<>();
    for (int i = 1; i <= columnCount; i++) {
      String columnName = metaData.getColumnName(i);
      columnNameList.add(columnName);
    }
    while (rs.next()) {
      map.put(rowNumName, rowNum);
      for (int i = 1; i <= columnCount; i++) {
        Object object = rs.getString(i);
        map.put(columnNameList.get(i - 1), object);
      }
    }
    return map;
  }

  static class DbPreparedStatementCreator implements PreparedStatementCreator {

    private final String sql;
    List<Object> values;

    public DbPreparedStatementCreator(String sql, List<Object> values) {
      this.sql = sql;
      this.values = values;
    }

    @Override
    public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
      PreparedStatement psst = con.prepareStatement(sql, new String[]{"id"});
      if (!CollectionUtils.isEmpty(values)) {
        for (int i = 0; i < values.size(); i++) {
          psst.setObject(i + 1, values.get(i));
        }
      }
      return psst;
    }
  }

  class MapMapper implements ResultSetExtractor<Map<String, Object>> {

    private String rowNumName = "rowNum";

    public MapMapper(String rowNumName) {
      if (StringUtils.isNotBlank(rowNumName)) {
        this.rowNumName = rowNumName;
      }
    }

    @Override
    public Map<String, Object> extractData(ResultSet rs) throws SQLException, DataAccessException {
      return getStringObjectMap(rs, rowNumName, 0);
    }
  }
}
